/*
 * Copyright (c) 2012 OptiCare Systems Ltd
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 *
 * Authors: Kevin Martin (kev82@khn.org.uk)
 */
#include <lua5.2/lua.hpp>
#include <lua5.2/lauxlib.h>
#include <curl/curl.h>
#include <map>
#include <string>
#include <stdexcept>
#include <cstdlib>
#include <sstream>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>

#ifdef DEBUG_CHILD
#include <syslog.h>
#endif

extern "C" {
	int luaopen_curleasy(lua_State *l);
}

typedef std::map<std::string, std::string> MSS;

//Takes a map and converts to a get/post string
std::string varString(const MSS &m, bool encode = true) {
	if(m.empty()) return "";

	CURL *c = curl_easy_init();

	std::ostringstream oss;

	MSS::const_iterator i = m.begin();
	oss << i->first << "=";
	if(encode) {
		char *t2 = curl_easy_escape(c, i->second.c_str(), 0);
		oss << t2;
		curl_free(t2);
	} else {
		oss << i->second;
	}
	++i;
		
	while(i != m.end()) {
		oss << i->first << "=";
		if(encode) {
			char *t2 = curl_easy_escape(c, i->second.c_str(), 0);
			oss << t2;
			curl_free(t2);
		} else {
			oss << i->second;
		}
		++i;
	}

	curl_easy_cleanup(c);

	return oss.str();
}

//We pass this to curl to grab the whole output
static size_t appendString(void *ptr, size_t n, size_t nmemb, void *str) {
    std::string &x = *reinterpret_cast<std::string *>(str);

    size_t bytes = nmemb * n;
    char *data = reinterpret_cast<char *>(ptr);
    x += std::string(data, data+bytes);

    return bytes;
}

//This contains the actual code to do the connection, it runs in the child
//If you're not worried about curl_global_init, you can get rid of the fork,
//and just call this.
static std::string childConnect(const std::string &url,
 const MSS &headers, const MSS &get, const MSS &post,
 const MSS &options) {
	curl_global_init(CURL_GLOBAL_ALL);

    CURL *curl;
    CURLcode res;

    curl = curl_easy_init();
    if(!curl) {
        throw std::runtime_error("failed to create curl easy session");
    }

	if(options.count("verbose") == 1) {
		curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
	}

	if(options.count("addheaders") == 1) {
		curl_easy_setopt(curl, CURLOPT_HEADER, 1);
	}

    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());

    if(!get.empty()) {
        std::string temp = url + "?" + varString(get, true);
        curl_easy_setopt(curl, CURLOPT_URL, temp.c_str());
    }

    if(!post.empty()) {
		std::string temp = varString(post, true);
        curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, temp.c_str());
    }

    curl_slist *header_slist = NULL;
    if(!headers.empty()) {
		MSS::const_iterator i;
		for(i=headers.begin();i!=headers.end();++i) {
			std::ostringstream oss;
			oss << i->first << ": " << i->second;
            header_slist = curl_slist_append(header_slist, oss.str().c_str());
		}
        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_slist);
    }

    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION,
     appendString);
    std::string output = "";
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &output);

    res = curl_easy_perform(curl);
    if(header_slist != NULL) curl_slist_free_all(header_slist);

    if(res != 0) {
        throw std::runtime_error("Curl failed");
    }

    curl_easy_cleanup(curl);
	curl_global_cleanup();

    return output;
}

static std::string realConnect(const std::string &url,
 const MSS &headers, const MSS &get, const MSS &post,
 const MSS &options) {
	int pipe_ends[2];

	if(pipe(pipe_ends) != 0) {
		throw std::runtime_error("Failed Pipe");
	}

	int child = fork();
	if(child == -1) {
		throw std::runtime_error("Failed Fork");
	}

	if(child == 0) {
		close(pipe_ends[0]);
		try {
			std::string output = childConnect(url, headers, get, post, options);

			const char *buffer = output.c_str();
			int sz = output.size();
	 
			while(true) {
				int bytes = write(pipe_ends[1], buffer, sz);
				if(bytes == -1) {
					throw std::runtime_error("Writing Error");
				}
				if(bytes == sz) break;
		
				buffer += bytes;
				sz -= bytes;
			}
		} catch (std::runtime_error &e) {
#ifdef DEBUG_CHILD
			syslog(LOG_EMERG, e.what());
#endif
		}

		close(pipe_ends[1]);
		exit(0);
	}

	if(child > 0) {
		//In parent
		close(pipe_ends[1]);

		std::ostringstream oss;
		char buffer[1024];
		while(true) {
			int bytes = read(pipe_ends[0], buffer, 1000);
			if(bytes == -1) {
				throw std::runtime_error("Read failed");
			}
			if(bytes == 0) break;
			buffer[bytes] = 0;
			oss << buffer;
		}

		waitpid(child, NULL, 0);
		return oss.str();
	}

	assert("Unreachable Code" == 0x0);
}

static int table2Map(lua_State *l, int tblidx,
 MSS &m) {
	tblidx = lua_absindex(l, tblidx);

	if(!lua_istable(l, tblidx)) {
        luaL_error(l, "Expexcted Table");
    }

    lua_pushnil(l);
    while (lua_next(l, tblidx) != 0) {
        if(!lua_isstring(l, -2)) {
            luaL_error(l, "Header Table has integer key");
        }
        if(!lua_isstring(l, -1)) {
            luaL_error(l, "Header Table has non-string value");
        }

        m[lua_tostring(l, -2)] = lua_tostring(l, -1);

        lua_pop(l, 1);
    }
}
	
static int connect(lua_State *l) {
	if(lua_gettop(l) < 1 || lua_gettop(l) > 2) {
		luaL_error(l, "Expected 1 or 2 arguments");
	}

	std::string url = luaL_checkstring(l, 1);
	
	MSS getfields, postfields, headers;
	MSS options;

	if(lua_gettop(l) == 2) {
		luaL_checktype(l, 2, LUA_TTABLE);

		lua_pushstring(l, "getfields");
		lua_gettable(l, -2);
		if(lua_type(l, -1) != LUA_TNIL) {
			table2Map(l, -1, getfields);
		}
		lua_pop(l, 1);

		lua_pushstring(l, "postfields");
		lua_gettable(l, -2);
		if(lua_type(l, -1) != LUA_TNIL) {
			table2Map(l, -1, postfields);
		}
		lua_pop(l, 1);
		
		lua_pushstring(l, "reqheaders");
		lua_gettable(l, -2);
		if(lua_type(l, -1) != LUA_TNIL) {
			table2Map(l, -1, headers);
		}
		lua_pop(l, 1);

		lua_pushstring(l, "options");
		lua_gettable(l, -2);
		if(lua_type(l, -1) != LUA_TNIL) {
			table2Map(l, -1, options);
		}
		lua_pop(l, 1);
	}

	try {
		std::string response = realConnect(url, headers,
		 getfields, postfields, options);

		lua_pushstring(l, response.c_str());
		return 1;
	} catch (std::exception &e) {
		luaL_error(l, e.what());
	}

	assert("Unreachable Code" == 0x0);
} 

static const luaL_Reg curleasy_funcs[] = {
	{"connect", connect},
	{NULL, NULL}
};

int luaopen_curleasy(lua_State *l) {
	luaL_newlib(l, curleasy_funcs);
	return 1;
}
